import pandas as pd
import os
from sklearn.metrics.pairwise import cosine_similarity
import numpy as np
import re
from IPython.display import clear_output
import matplotlib.pyplot as plt
from sklearn.metrics import det_curve, DetCurveDisplay
pd.set_option('display.max_colwidth', None)
from sklearn.decomposition import PCA
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis as LDA
from sklearn.preprocessing import StandardScaler
import plotly.graph_objects as go
import pickle
import plotly
Zastosowanie post-processingu na danych.¶
W raporcie stosuję trzy różne rodzaje post-processingu: centrowanie danych, PCA i LDA. Tego rodzaju post-processing stosuje się, aby wykorzystać również wartości ujemne. Wektory, które są reprezentacją naszych nagrań, zarówno enrollment, jak i ewaluacyjnych, mają wartości wyłącznie w zakresie od 0 do 1. Dzieje się tak, ponieważ wartości te są prawdopodobieństwami przynależności próbek do komponentów w modelu GMM. Dzięki zastosowaniu post-processingu możemy wydobyć więcej informacji z naszych danych.
Wczytywanie embeddingów.¶
#ścieżka do folderu gdzie przechowywane są dane z embeddingiem
embedded_data_folder = "C:/Users/zbugo/Desktop/praktyki_zadania/13/embedded_data"
#pobranie wszystkich nazw folderów w podanym katalogu - czyli do folderów każdej osoby
subfolders = [f for f in os.listdir(embedded_data_folder) if os.path.isdir(os.path.join(embedded_data_folder, f))]
#tworzę ścieżki do embeddingów każdej z osób
paths_with_ID = [embedded_data_folder + '/' + subfolder for subfolder in subfolders]
#sortuję ścieżki według ID danej osoby
paths_with_ID = sorted(paths_with_ID, key=lambda x: int(x.split('embedded_data')[-1]))
Powyższy kod wczytuje ścieżki do embeddingów 25 kobiet i 25 mężczyzn. Dane te zostały przygotowane w poprzednich raportach.
paths_with_ID[0:5]
['C:/Users/zbugo/Desktop/praktyki_zadania/13/embedded_data/embedded_data0', 'C:/Users/zbugo/Desktop/praktyki_zadania/13/embedded_data/embedded_data1', 'C:/Users/zbugo/Desktop/praktyki_zadania/13/embedded_data/embedded_data2', 'C:/Users/zbugo/Desktop/praktyki_zadania/13/embedded_data/embedded_data3', 'C:/Users/zbugo/Desktop/praktyki_zadania/13/embedded_data/embedded_data4']
Możemy zobaczyć, jak wyglądają ścieżki do embeddingów.
#lista na ramki z embedingami każdej z osób
list_for_eval_data_frames = []
#ramka przechowująca wszytkie emgadding enrollment
data_frame_enroll_data = pd.DataFrame()
#iterator tworzę by śledzić postęp działania pęti
iterator = 0
for path_with_ID in paths_with_ID:
#tworzę ścieżki do embedding enrollment każdej z osób
path_to_enroll = path_with_ID + '/enroll/embedded_enrol' + str(iterator) + '.csv'
#wczytuję embedding enrollment i iteracyjnie dodaje go do ramki - wiersze to enrollmenty osób
data_frame_enroll_data = pd.concat([data_frame_enroll_data, pd.read_csv(path_to_enroll)], ignore_index=True)
#wyciągam ścieżki do nagrań testowych danej osoby
path_to_eval = path_with_ID + '/eval/'
all_paths_to_eval = [f for f in os.listdir(path_to_eval) if os.path.isfile(os.path.join(path_to_eval, f))]
all_paths_to_eval = [path_to_eval + path for path in all_paths_to_eval]
#sortuję ścieżki, aby były uporządkowane według ID nagrania testowego
all_paths_to_eval = sorted(all_paths_to_eval, key=lambda x: int(re.search(r'(\d+)(?=\.csv)', x).group()))
#tworzę ramkę danych w której wiersze będą embeddingi eval JEDNEJ OSOBY - wiersze to różne nagrania tej samej osoby
df_eval = pd.DataFrame()
#iteracyjnie dorzucam nagrania
for path in all_paths_to_eval:
df_eval = pd.concat([df_eval, pd.read_csv(path)], ignore_index=True)
#do listy wrzucam ramki, gdzie każda ramka odpodiada jednej osobie
list_for_eval_data_frames.append(df_eval)
#śledzę postęp działania pętli
clear_output(wait=True)
iterator = iterator + 1
print(iterator/len(paths_with_ID))
1.0
W powyższym bloku kodu wczytuję embeddingi. Dla enrollment istnieje jedna ramka danych, w której każdy wiersz odpowiada innej osobie. Ewaluacyjne nagrania są przechowywane w liście — każdy element listy to ramka danych odpowiadająca jednej osobie. W tej ramce (jednej z listy), wiersze reprezentują nagrania (oczywiście wszystkie nagrania/wiersze należą do jednej osoby).
data_frame_enroll_data.head()
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ... | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0.016061 | 0.037225 | 0.033713 | 0.010862 | 0.000000 | 0.017374 | 3.051719e-02 | 0.002731 | 8.254007e-11 | 0.009259 | ... | 4.627383e-02 | 0.026662 | 0.025655 | 0.001715 | 0.000896 | 0.000698 | 0.017517 | 0.000243 | 0.056859 | 0.020914 |
| 1 | 0.004948 | 0.010180 | 0.014570 | 0.042996 | 0.096595 | 0.054379 | 6.245632e-03 | 0.000186 | 9.793247e-15 | 0.003365 | ... | 3.510889e-04 | 0.013322 | 0.014190 | 0.004361 | 0.001310 | 0.001961 | 0.019742 | 0.000064 | 0.085954 | 0.024428 |
| 2 | 0.010735 | 0.022861 | 0.057958 | 0.018219 | 0.000000 | 0.014890 | 2.277481e-16 | 0.000432 | 5.257036e-04 | 0.016517 | ... | 8.871839e-03 | 0.010189 | 0.019216 | 0.002227 | 0.000803 | 0.000264 | 0.028128 | 0.000024 | 0.033623 | 0.051148 |
| 3 | 0.014271 | 0.070275 | 0.049537 | 0.003289 | 0.000000 | 0.031889 | 8.669394e-04 | 0.004757 | 2.212869e-05 | 0.024013 | ... | 1.005580e-07 | 0.010308 | 0.001131 | 0.000296 | 0.009197 | 0.000155 | 0.001437 | 0.000191 | 0.019623 | 0.012423 |
| 4 | 0.006863 | 0.018319 | 0.028656 | 0.014880 | 0.023183 | 0.007494 | 2.514921e-03 | 0.002040 | 1.211667e-29 | 0.002670 | ... | 6.984625e-04 | 0.045256 | 0.046806 | 0.003278 | 0.001018 | 0.000615 | 0.017684 | 0.000967 | 0.015770 | 0.008685 |
5 rows × 64 columns
list_for_eval_data_frames[0].head()
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ... | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0.072280 | 0.048442 | 0.062727 | 0.004963 | 0.0 | 0.020369 | 4.285654e-97 | 0.000307 | 4.006581e-14 | 0.000481 | ... | 0.001935 | 0.011524 | 0.017425 | 0.001197 | 0.000339 | 0.000534 | 0.009788 | 0.000208 | 0.076893 | 0.053448 |
| 1 | 0.066883 | 0.028970 | 0.052333 | 0.002179 | 0.0 | 0.024819 | 0.000000e+00 | 0.000579 | 2.239713e-28 | 0.000163 | ... | 0.000769 | 0.008180 | 0.009396 | 0.001423 | 0.000335 | 0.000395 | 0.015831 | 0.000272 | 0.064757 | 0.037688 |
| 2 | 0.034561 | 0.051490 | 0.040978 | 0.004110 | 0.0 | 0.018776 | 0.000000e+00 | 0.001204 | 7.604111e-20 | 0.000099 | ... | 0.008862 | 0.028086 | 0.023853 | 0.000278 | 0.000380 | 0.000697 | 0.004044 | 0.000598 | 0.053900 | 0.028599 |
| 3 | 0.039741 | 0.031169 | 0.057277 | 0.003557 | 0.0 | 0.017032 | 0.000000e+00 | 0.001483 | 6.741872e-15 | 0.001204 | ... | 0.000329 | 0.016589 | 0.018547 | 0.000514 | 0.000497 | 0.005325 | 0.009947 | 0.000545 | 0.052856 | 0.027626 |
| 4 | 0.033711 | 0.029811 | 0.061313 | 0.001620 | 0.0 | 0.033861 | 0.000000e+00 | 0.001471 | 1.502588e-16 | 0.000011 | ... | 0.002871 | 0.023060 | 0.012113 | 0.000276 | 0.001265 | 0.000121 | 0.001894 | 0.000493 | 0.075609 | 0.019588 |
5 rows × 64 columns
Jest 64 kolumny, ponieważ wybrałem tyle komponentów w modelu GMM.
#ramka gdzie każdy wiersz jest embeddingiem innego nagrania z zestawu uczącego model UBM
with open("mean_predict_proba_df.pkl", 'rb') as file:
mean_predict_proba_df = pickle.load(file)
Wczytuję embeddingi nagrań, na których trenowałem model UBM.
Funkcja do cross-checkingu i listy potrzebne na przestrzeni całego raportu.¶
def cross_checking(enroll_data, list_eval_data):
#lista na score dla wszystkich osób
score_list_for_all_person = []
#liasta na id osoby do której należy nagranie enrollment
id_enroll = []
#analogicznie, z tym że do kogo należy nagranie ewaluacyjnie
id_eval = []
#lista typu bool, informująca czy była to próba logowania przez autorywanego użytkownika czy próba włamania
is_genuine = []
#w pętli liczę podobieństwo kosinusowne które będzie oceniać podobieńtwo enrollmentu i nagrania ewaluacyjnego
for i in range(0, len(enroll_data)):
#pobieram pierwszy embedding enrollment
emdedding_enrollment = pd.DataFrame(enroll_data.loc[i]).T
#lista w której będzie sprawdzane podobieńtwo jednego enrollmentu ze wszystkimi nagraniami ewaluacyjnymi
score_list_for_one_person = []
#w pętli liczę podobieńśtwo kosinusowe enrollmentu z nagraniami ewaluacyjnymi
for j in range(0, len(list_eval_data)):
#biorę jedno nagranie ewaluacyjne
df_eval = list_eval_data[j]
#liczę podobieństwo kosinusowe pomiedzy nagraniem enrollment i ewaluacyjnym
cos_sim = cosine_similarity(emdedding_enrollment, df_eval)[0]
#wrzucam je do listy
score_list_for_one_person.extend(cos_sim)
#zapisuję czyje było nagranie enrollmentowe
id_enroll.extend([i] * len(cos_sim))
#zapisuję czyje było nagranie ewaluacyjne
id_eval.extend([j] * len(cos_sim))
#zapisuję czy była to próba włamania czy logowania użytkownika autoryzowanego
is_genuine.extend([i==j] * len(cos_sim))
#listę w której sprawdzone są podobieństwa JEDNEGO nagrania enrollment do wszystkich ewaluacyjnych wrzucam do listy
#na koniec będzie zawierać tyle list ile jest nagrań enrollment, a każdy element listy będzie to lista podbieństw jednego enrollmentu do ewaluacyjnych
score_list_for_all_person.extend(score_list_for_one_person)
#wszyskie wcześniej zapisane rzeczy zapisuję w jednej ramce
df_scores = pd.DataFrame({
'id_enroll' : id_enroll,
'id_eval' : id_eval,
'is_genuine' : is_genuine,
'score' : score_list_for_all_person
})
#funkcja zwraca wyżej stworzoną ramkę
return df_scores
Powyższa funkcja służy do cross-checkingu. Przyjmuje ramkę danych enroll_data – embeddingi enrollment, gdzie każdy wiersz odpowiada innej osobie, oraz list_eval_data – listę, w której każdy element to ramka danych odpowiadająca innej osobie. W tych ramkach wiersze reprezentują nagrania ewaluacyjne należące do jednej osoby.
df_curve_list = []
colors = []
postprocessing_name = []
all_data = []
list_for_FRR_FAR = []
Listy, które zostaną wykorzystane do każdego rodzaju post-processingu.
Dane bez zastosowania post-processingu.¶
#wczytuję ramkę z cross-cheskingiem stworzonym w poprzenich raportach
without_postprocessing = pd.read_csv('C:/Users/zbugo/Desktop/praktyki_zadania/14/long_data_frame_2.csv')
#dzielę powyższą ramkę na dwie - jedna odpowiada próbom logowania się przez osobę autoryzowaną a druga próbom włamania
genuine = without_postprocessing[without_postprocessing['genuine']]
impostor = without_postprocessing[~without_postprocessing['genuine']]
Wczytuję dane po cross-checkingu, co zostało zrobione w poprzednich raportach. Następnie dzielę je na dwie ramki: te, gdzie była próba włamania, i te, gdzie logował się użytkownik autoryzowany.
genuine.head()
| ID_enrollment | enrollment_file | ID_test | test_file | genuine | score | |
|---|---|---|---|---|---|---|
| 0 | 0 | embedded_enrol0.csv | 0 | embedded_eval0.csv | True | 0.842341 |
| 1 | 0 | embedded_enrol0.csv | 0 | embedded_eval1.csv | True | 0.787785 |
| 2 | 0 | embedded_enrol0.csv | 0 | embedded_eval6.csv | True | 0.797577 |
| 3 | 0 | embedded_enrol0.csv | 0 | embedded_eval8.csv | True | 0.841496 |
| 4 | 0 | embedded_enrol0.csv | 0 | embedded_eval10.csv | True | 0.818735 |
Ramka impostor wygląda analogicznie, z tym że wszystkie wartości dla genuine będą oznaczone jako false, a wynik (score) będzie mniejszy.
data = genuine, impostor
all_data.append(data)
Umieszczam je w liście, ponieważ będą potrzebne podczas tworzenia wykresów.
#liczę FAR i FRR
FAR, FRR, thresholds = det_curve(y_true=without_postprocessing['genuine'], y_score=without_postprocessing['score'])
#szukam punktu EER, czyli tam gdzie krzywe FAR i FRR się przecinają
arg_of_best_threshold = np.argmin(np.abs(FAR - FRR))
#zapisuję wartości FAR i FRR wraz z thresholdem w taki spsób, aby łatwo później było stworzyć wykresy
df_curve = pd.DataFrame({
'False Acceptance Rate': FAR[::10],
'False Rejection Rate': FRR[::10]
}, index=thresholds[::10])
df_curve.index.name = "Thresholds"
df_curve.columns.name = "Rate"
#zapisuję wszytskie rzeczy które policzyłem i nadaję pewne cechy post-processingowi aby był rozróżnialny na wykresie
df_curve_list.append(df_curve)
colors.append('green')
postprocessing_name.append('no postproces')
FAR_FRR = FAR, FRR
list_for_FRR_FAR.append(FAR_FRR)
W powyższym kodzie obliczane są FAR i FRR, a także zapisywane są charakterystyczne cechy danego post-processingu, które będą wykorzystane przy tworzeniu wykresów (np. kolor, nazwa post-processingu itp.). Ogólnie przygotowuję dane do narysowania wykresów.
Centrowanie danych.¶
Od tego miejsca nie będę już tak szczegółowo opisywać działania kodu, ponieważ będzie on identyczny lub analogiczny do tego opisanego wyżej.¶
#ramka danych na zcentrowane dane enrollmentowe
pd_centered_enroll = pd.DataFrame()
#liczę średnie ze zbioru na którym nauczyłem model UBM, średnie dla każdego komponetu
to_subtract = pd.DataFrame(mean_predict_proba_df.mean()).T.loc[0].values
#w pętli odejmuje wyżej policzone średnie od danych embedded enrollment
for i in range(0, len(data_frame_enroll_data)):
row = pd.DataFrame(data_frame_enroll_data.loc[i].values - to_subtract).T
pd_centered_enroll = pd.concat([pd_centered_enroll, row], ignore_index=True)
#analogiczna lista do tej która przetrzymuje dane ewaluacyjne, z tym że ta będzie przetrzymywać już zcentrowane dane
list_centered_eval_data = []
#w petli centruje nagrania ewaluacyjne
#pętla zewnętrzna bierze ramki w których są wszystkie nagrania ewaluacyjne danej osoby
#pętla wenętrzna bierze każdy wiersz z ramki, czyli każde nagranie ewaluacyjne danej osoby
for df in list_for_eval_data_frames:
df_for_one_person = pd.DataFrame()
for i in range(0, len(df)):
one_row = df.loc[i].values
one_row = pd.DataFrame(one_row - to_subtract).T
df_for_one_person = pd.concat([df_for_one_person, one_row], ignore_index=True)
list_centered_eval_data.append(df_for_one_person)
Powyższy fragment kodu centruje dane, zarówno embeddingi enrollment, jak i embeddingi ewaluacyjne.
df_scores = cross_checking(enroll_data=pd_centered_enroll, list_eval_data=list_centered_eval_data)
Przeprowadzam cross-checking scentrowanych danych.
genuine = df_scores[df_scores['is_genuine']]
impostor = df_scores[~df_scores['is_genuine']]
data = genuine, impostor
all_data.append(data)
FAR, FRR, thresholds = det_curve(y_true=df_scores['is_genuine'], y_score=df_scores['score'])
arg_of_best_threshold = np.argmin(np.abs(FAR - FRR))
y = np.mean(a = np.array(FAR[arg_of_best_threshold], FRR[arg_of_best_threshold]))
x = thresholds[arg_of_best_threshold]
df_curve = pd.DataFrame({
'False Acceptance Rate': FAR[::10],
'False Rejection Rate': FRR[::10]
}, index=thresholds[::10])
df_curve.index.name = "Thresholds"
df_curve.columns.name = "Rate"
df_curve_list.append(df_curve)
colors.append('orange')
postprocessing_name.append('centered data')
FAR_FRR = FAR, FRR
list_for_FRR_FAR.append(FAR_FRR)
PCA.¶
#Tworzę skaler, aby poprawnie policzyć PCA
scaler = StandardScaler()
scaler.fit(mean_predict_proba_df.values)
#skaluję embedding danych na których nauczony został model
scaled_mean_predict_proba_df = pd.DataFrame(scaler.transform(mean_predict_proba_df.values))
#skaluję embedding enrollment
scaled_enroll = pd.DataFrame(scaler.transform(data_frame_enroll_data.values))
#skaluje również dane ewaluacyjne
scaled_eval_list = []
for eval_df in list_for_eval_data_frames:
scaled_one_person_eval = scaler.transform(eval_df.values)
scaled_eval_list.append(pd.DataFrame(scaled_one_person_eval))
Powyżej obliczam skaler i skaluję embeddingi danych, na których został nauczony model UBM, embeddingi enrollment oraz embeddingi ewaluacyjne.
pca = PCA(n_components=0.99, svd_solver='full')
pca.fit(scaled_mean_predict_proba_df.values)
pca_enroll = pca.transform(scaled_enroll.values)
pca_enroll = pd.DataFrame(pca_enroll)
pca_eval_list = []
for scaled_eval_df in scaled_eval_list:
pca_eval_df = pca.transform(scaled_eval_df.values)
pca_eval_list.append(pd.DataFrame(pca_eval_df))
Ustawiłem n_components=0.99 i svd_solver='full', ponieważ uważam, że 99% wyjaśnionej wariancji to wystarczający poziom, a dodatkowo może to usunąć znaczną część niepotrzebnych głównych składowych. Ostatecznie nie zależy mi na redukcji wymiarowości, ponieważ nie jest to celem. Celem natomiast jest jak najlepsza dyskryminacja mówców. Robię to z ciekawości, aby sprawdzić, ile głównych składowych zostanie usuniętych.
pca_enroll.shape
(50, 60)
Widzimy, że PCA zredukowało nasze dane tylko o 4 wymiary, tracąc jedynie 1% wyjaśnionej wariancji. Jednak gdybyśmy byli w stanie zaakceptować nieco większą utratę wyjaśnionej wariancji, moglibyśmy znacznie bardziej zredukować wymiarowość naszych danych.
df_scores = cross_checking(enroll_data=pca_enroll, list_eval_data=pca_eval_list)
genuine = df_scores[df_scores['is_genuine']]
impostor = df_scores[~df_scores['is_genuine']]
data = genuine, impostor
all_data.append(data)
FAR, FRR, thresholds = det_curve(y_true=df_scores['is_genuine'], y_score=df_scores['score'])
arg_of_best_threshold = np.argmin(np.abs(FAR - FRR))
y = np.mean(a = np.array(FAR[arg_of_best_threshold], FRR[arg_of_best_threshold]))
x = thresholds[arg_of_best_threshold]
df_curve = pd.DataFrame({
'False Acceptance Rate': FAR[::10],
'False Rejection Rate': FRR[::10]
}, index=thresholds[::10])
df_curve.index.name = "Thresholds"
df_curve.columns.name = "Rate"
df_curve_list.append(df_curve)
colors.append('purple')
postprocessing_name.append('pca')
FAR_FRR = FAR, FRR
list_for_FRR_FAR.append(FAR_FRR)
LDA.¶
#liczę LDA wykorzsytując dane które użyłem do nauki modelu UBM, z tym że są one po ekstracji (embedding)
lda = LDA()
lda.fit(X=scaled_mean_predict_proba_df.values, y=mean_predict_proba_df.index)
lda_enroll = lda.transform(scaled_enroll.values)
lda_enroll = pd.DataFrame(lda_enroll)
lda_eval_list = []
for scaled_eval_df in scaled_eval_list:
lda_eval_df = lda.transform(scaled_eval_df.values)
lda_eval_list.append(pd.DataFrame(lda_eval_df))
Tym razem nie zmieniam żadnych parametrów dla LDA, ponieważ nie zależy mi na redukcji wymiarowości ani na wydajności, gdyż LDA i tak liczy się bardzo szybko. Dlatego skorzystam z domyślnych parametrów.
df_scores = cross_checking(enroll_data=lda_enroll, list_eval_data=lda_eval_list)
genuine = df_scores[df_scores['is_genuine']]
impostor = df_scores[~df_scores['is_genuine']]
data = genuine, impostor
all_data.append(data)
FAR, FRR, thresholds = det_curve(y_true=df_scores['is_genuine'], y_score=df_scores['score'])
arg_of_best_threshold = np.argmin(np.abs(FAR - FRR))
y = np.mean(a = np.array(FAR[arg_of_best_threshold], FRR[arg_of_best_threshold]))
x = thresholds[arg_of_best_threshold]
df_curve = pd.DataFrame({
'False Acceptance Rate': FAR[::10],
'False Rejection Rate': FRR[::10]
}, index=thresholds[::10])
df_curve.index.name = "Thresholds"
df_curve.columns.name = "Rate"
df_curve_list.append(df_curve)
colors.append('red')
postprocessing_name.append('lda')
FAR_FRR = FAR, FRR
list_for_FRR_FAR.append(FAR_FRR)
Wykresy.¶
fig, axes = plt.subplots(2, 2, figsize=(18, 8))
for i in range(0, len(all_data)):
row = i // 2 # Wiersz kratki (0 lub 1)
col = i % 2 # Kolumna kratki (0 lub 1)
# Wybieramy odpowiedni subplot
ax = axes[row, col]
ax.hist(all_data[i][0]['score'], bins = 128, alpha = 0.5, label='geniune')
ax.hist(all_data[i][1]['score'], bins = 128, alpha = 0.5, label='impostor')
ax.set_xlim(-1, 1)
ax.legend()
ax.set_title(postprocessing_name[i])
ax.grid()
Widzimy od razu, że wartości score są różne dla różnych rodzajów post-processingu. Pierwszy wykres, zatytułowany 'no postprocess', przedstawia rozkład wartości score bez żadnego post-processingu, i widzimy, że wartości score mieszczą się w zakresie [0, 1]. Zcentrowane dane nie ograniczają się już do tego zakresu, ponieważ mogą być również mniejsze od 0. To samo dotyczy histogramów dla PCA i LDA. Chociaż zmiany w danych są widoczne już na histogramach, bardziej interesujące będą wartości EER, ponieważ to one ostatecznie wskazują, jak dobry jest model.
plotly.offline.init_notebook_mode()
# Lista kolorów dla różnych modeli
# Tworzenie pustego wykresu
fig_thresh = go.Figure()
# Iteracyjne dodawanie krzywych do wykresu
for i, df_curve in enumerate(df_curve_list):
legend_group = postprocessing_name[i] # Grupa legendy dla modelu
# Dodanie linii dla FAR (z przypisaną grupą legendy)
fig_thresh.add_trace(go.Scatter(
x=df_curve.index, y=df_curve['False Acceptance Rate'],
mode='lines',
line=dict(color=colors[i]),
name='FAR',
legendgroup=legend_group, # Przypisanie do grupy
showlegend=False # Ukrycie wpisu dla FAR w legendzie
))
# Dodanie linii dla FRR (z przypisaną grupą legendy)
fig_thresh.add_trace(go.Scatter(
x=df_curve.index, y=df_curve['False Rejection Rate'],
mode='lines',
line=dict(color=colors[i]), # Inny styl dla FRR (przerywana linia)
name='FRR',
legendgroup=legend_group, # Przypisanie do grupy
showlegend=False # Ukrycie wpisu dla FRR w legendzie
))
# Znalezienie najlepszego threshold (EER)
arg_of_best_threshold = np.argmin(np.abs(df_curve['False Acceptance Rate'] - df_curve['False Rejection Rate']))
x = df_curve.index[arg_of_best_threshold]
y = np.mean([df_curve['False Acceptance Rate'].iloc[arg_of_best_threshold], df_curve['False Rejection Rate'].iloc[arg_of_best_threshold]])
# Dodanie punktu dla EER (z przypisaną grupą legendy)
fig_thresh.add_trace(go.Scatter(
x=[x], y=[y],
mode='markers',
marker=dict(size=10, color=colors[i]),
name='EER',
legendgroup=legend_group, # Przypisanie do grupy
showlegend=False # Ukrycie wpisu dla EER w legendzie
))
# Dodanie tylko jednego wpisu do legendy dla całego modelu
fig_thresh.add_trace(go.Scatter(
x=[None], y=[None], # Wpis do legendy bez dodawania nowych danych
mode='lines',
line=dict(color=colors[i]),
name=postprocessing_name[i], # Nazwa modelu w legendzie
legendgroup=legend_group, # Przypisanie do tej samej grupy
showlegend=True # Pokaż w legendzie
))
# Dostosowanie osi i tytułu
fig_thresh.update_layout(
title="FRR, FAR EER for types of post-processing",
xaxis_title="Thresholds",
width=1200,
height=600
)
# Wyświetlenie wykresu
fig_thresh.show()
Wykres jest interaktywny — można przybliżyć widok, naciskając LPM i tworząc okno, które stanie się nowym zakresem widoku. Można również wyłączać wykresy dla nieinteresujących nas post-processingów, klikając odpowiednie nazwy w legendzie, a także śledzić wartości krzywych.¶
Choć na pierwszy rzut oka różnice pomiędzy EER dla różnych post-processingów nie są zbyt widoczne, to po przybliżeniu danych stają się wyraźne. Model najlepiej radzi sobie z danymi po LDA, na drugim miejscu jest PCA, na trzecim centrowanie danych, a jak można było się domyślić, najgorzej wypadają dane bez post-processingu. Wykresy, oprócz umożliwienia odczytania EER, pozwalają śledzić zmianę odsetka fałszywych akceptacji i fałszywych odrzuceń przy konkretnych thresholdach.
plotly.offline.init_notebook_mode()
fig = go.Figure()
for i in range(0,len(list_for_FRR_FAR)):
FAR, FRR = list_for_FRR_FAR[i]
fig.add_trace(go.Scatter(x=FAR, y=FRR, name=postprocessing_name[i], mode='lines'))
fig.update_layout(
title='Krzywa DET',
xaxis_title='FAR',
yaxis_title='FRR',
width=1200,
height=600
)
fig.update_xaxes(range=[0, 0.35])
fig.update_yaxes(range=[0, 0.55])
fig.show()